Command Pattern(커맨드 패턴)

들어가기

이번 포스팅에서는 커맨드 패턴(Command Pattern) 에 대해서 알아보겠습니다.

본론

1. 커맨드 패턴이란 무엇인가

커맨드 패턴(Command Pattern)은 특정 행위(기능)을 캡슐화하여 클래스를 변경하지 않고 재사용할때 유용하게 사용합니다.

즉, 요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 매서드 이름, 매개변수 등 요청에 필요한 정보를 저장 또는 로깅, 취소할 수 있게 하는 패턴입니다.

말로는 많이 어려우니 밑에서 예시를 통해 커맨드 패턴(Command Pattern)을 사용하지 않았을때 발생하는 문제점과 커맨드 패턴(Command Pattern)을 사용했을때 얻는 이점을 다시 한번 확인해보겠습니다.

2. 왜 커맨드 패턴을 사용해야 하는가

한 가지 예를 들어겠습니다. 버튼이 두개 있는 리모컨이 있습니다. 하나는 Turn On, 다른 한 가지는 Turn Off 입니다. 하지만 이 리모컨은 만능 리모컨이기에 TV, 에어컨 등 모든 전자 기기를 끌수 있고 킬수 있다고 가정하겠습니다.

이때 어떤 전자 기기는 전원을 키고 끌때 내부적으로 모든 로직이 똑같을 수도 있고 다를 수도 있습니다. 즉, 이전 코드를 재사용 할 수도 있고 완전히 새로 구현해야 할 수도 있습니다.

이럴 경우 커맨드 패턴(Command Pattern)을 사용하면 기능 자체를 추상화하여 재사용성을 높이고 메서드를 직접 호출하지 않으면서 수정할 부분을 최소화 시킬수 있습니다.

3. 예시

커맨드 패턴(Command Pattern)의 필요성에서 보았듯이 만능 리모콘을 만들어보며 패턴을 적용하지 않았을 경우 문제점을 알아보겠습니다.

간략한 예시를 위해 turn on만 가정해서 코드를 작성하겠습니다.

3.1 커맨드 패턴을 적용하지 않았을 경우

public class Lamp {
    public void turnOn() {
        System.out.println("Turn On");
    }
}

public class Button {
    private Lamp theLamp;

    public Button(Lamp theLamp) {
        this.theLamp = theLamp;
    }

    public void pressed() {
        theLamp.turnOn();
    }
}

public class Client {
    public static void main(String[] args) {
        Lamp lamp = new Lamp();
        Button remote = new Button(lamp);
        remote.pressed();
    }
}

위에서 작성한 코드는 전등을 키는 간단한 코드입니다. 작동하는데는 문제가 없어 보입니다.

하지만 몇가지 문제점를 확인할 수 있습니다.

  • 같은 코드를 사용하면서 전등이 아닌 TV나 알람을 키는 기능을 추가할 경우
  • 버튼을 누르는 횟수에 따라 다른 기능을 수행해야 할 경우

첫 번째 문제점을 먼저 확인해보겠습니다. 위에 예시에서 볼수 있듯이 TV 클래스를 작성하는 것만으로 그치지 않고 기존 Button 클래스 코드를 수정해야 합니다. 이는 이전 포스팅에서 확인했듯이 OCP에 위배됩니다.

즉, 버튼을 눌렀을 때 지정된 기능만 고정적으로 수행하도록 만든 처음과 같은 설계는 기능이 추가될 때마다 여러 클래스를 수정해야하기에 OCP를 위반하는 설계입니다.

두 번째 경우의 문제점을 확인해보겠습니다.

예를 들어 버튼을 처음 눌렀을 때는 램프를 켜고 두 번 눌렀을 때는 알람을 동작하게 할 경우에 Button 클래스는 2가지 기능을 모두 구현할 수 있어야 합니다.

enum Mode {LAMP, ALARM};

public class Lamp {
  public void turnOn() {
      System.out.println("Turn On");
  }
}

public class Alarm {
  public void start() {
      System.out.println("Alarming");
  }
}

public class Button {
    private Lamp theLamp;
    private Alarm theAlarm;
    private Mode theMode;

    public Button(Lamp theLamp) {
        this.theLamp = theLamp;
        this.theAlarm = theAlarm;
    }

    public void setMode(Mode mode) {
        this.theMode = mode;
    }

    public void pressed() {
        swhitch(theMode) {
            case LAMP:
                theLamp.turnOn();
                break;
            case ALARM:
                theAlarm.start();
                break;
        }
    }
}

이 경우 역시 버튼을 눌렀을 때의 기능을 변경하기 위해 다시 Button 클래스의 코드를 수정해야합니다.

즉, OCP를 위배할 뿐더러 Button 클래스를 재사용하기 어렵습니다.

3.2 커맨드 패턴을 적용한 경우

새로운 기능을 추가하거나 변경하더라도 Button 클래스를 그대로 사용하려면 Button 클래스에 pressed 메서드에서 구체적인 기능을 직접 구현하는 대신 버튼을 눌렀을 때 실현될 기능을 클래스 외부에서 제공받아 캡슐화하여 호출하는 방법을 사용할 수 있습니다.

클래스 다이어그램을 보면 turnOn 메서드나 start 메서드를 직접 호출하지 않고 추상화하여 호출하고 있습니다.

public interface Command {
    public abstract void execute();
}

public class Alarm {
    public void start() {
        System.out.println("Alarming ... ");
    }
}

public class Lamp {
    public void turnOn() {
        System.out.println("Lamp On");
    }
}

public class AlarmOnCommand implements Command {
    private Alarm theAlarm;

    public AlarmOnCommand(Alarm theAlarm) {
        this.theAlarm = theAlarm;
    }

    @Override
    public void execute() {
        theAlarm.start();
    }
}

public class LampOnCommand implements Command {
    private Lamp theLamp;

    public LampOnCommand(Lamp theLamp) {
        this.theLamp = theLamp;
    }

    @Override
    public void execute() {
        theLamp.turnOn();
    }
}

public class Button {
    private Command theCommand;

    public Button(Command theCommand) {
        setTheCommand(theCommand);
    }

    public void setTheCommand(Command theCommand) {
        this.theCommand = theCommand;
    }

    public void pressed() {
        theCommand.execute();
    }
}

보시는 바와 같이 Command 인터페이스를 구현하는 LampOnCommand와 AlarmCommand 객체를 Button 객체에 설정하여 버튼을 눌렀을 때 필요한 임의의 기능은 Command 인터페이스를 구현한 클래스의 객체를 Button 객체에 설정해서 실행할 수 있습니다.

즉, 실행될 기능을 캡슐화함으로써 호출자(Invoker)와 실제 기능을 실행하는 수신자(Receiver) 클래스 사이의 의존성을 제거합니다. 따라서 실행될 기능의 변경에도 호출자 클래스를 수정 없이 그대로 사용할 수 있도록 해줍니다.

  • Command : 실행될 기능을 execute 메서드로 선언함
  • ConcreteCommand : 실제로 실행되는 기능을 구현
  • Invoker : 기능의 실행을 요청하는 호출자(Button)
  • Receiver : Concrete Command의 기능을 실행하기 위해 사용하는 수신자 클래스

마치며

커맨드 패턴(Command Pattern)의 개념과 관련해 추가적인 질문이나 오류, 오타가 있을시 댓글로 남겨주세요.

출처

JAVA 객체지향 디자인패턴

Share